Реализация алгоритма нахождения 94 цифр числа e.
Первый вариант реализации вычисления цифр числа e указан в журнале Наука и Жизнь №4 1990 г.: вырезка в формате djvu. Там получилось только 27 цифр. Через год с небольшим опубликовали вариант, где цифр уже стало 92: Наука и жизнь №6 1991.
По сути, это уже максимум. При вычислениях (нахождение остатка от деления), чтобы не потерять точность при переполнении, приходится хранить только 6 цифр, а не 7. В результате получаем 15 регистров × 6 знаков = 90. Начало используется с переполнением, поэтому 91 цифра после запятой или всего 92.
Текущая реализация – это немного улучшенный вариант того, что было опубликовано в 1991:
С учетом того, что в конце вычислений уже можно не боятся переполнений, в последний регистр дописаны ещё две цифры числа e. Так и получается 94 цифры.
Сначала поясню загадочное число 65, используемое в программе, но мало освещённое в исходной статье. Дело в том, что точность суммы сходящегося ряда (суммы), определяется последним членом. При вычислении e по формуле ряда ∑ 1⁄N!, точность определяется последним 1⁄N! Для определения разрядности (десятичной) числа используется десятичный логарифм. Количество разрядов у числа (точность) определяется значением логарифма. Если учесть, что N! = 1×2×3×…×N, а логарифм произведения равен сумме логарифмов отдельных членов, то достаточно вычислить lg(1)+lg(2)+…+lg(N) для получения количества знаков у числа N! Например, если результат больше трех, то число содержит больше трех знаков (четыре).
Для вычислений суммы логарифмов составим простую программу:
| # | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
|---|---|---|---|---|---|---|---|---|---|---|
| 00 | | x→П0 | Cx | П→x0 | FLg | + | FL0 | 02 | С/П | ||
На входе число N, на выходе сумма логарифмов. Зададим 4 и получим 1.3802112, т.е. 2 знака, как и ожидалось: 1×2×3×4 = 24.
А если задать 65, то получим 90.916331, те самые 91 знак после запятой. Вот откуда это число 65 – это предел суммы ряда, чтобы получилась 91 точная цифра.
Используя разложение в цепную дробь, на каждом шаге вычисляем 1/N. Для дальнейшего понимания, как делим на N частями, используем простой пример. Путь хотим вычислить 1/73. Возьмём вместо единицы 108 и поделим группами по 2 знака (102=100).
| Делимое | Делитель | Целочисленный результат | Остаток |
|---|---|---|---|
| 100 | 73 | 01 | 27 |
| 2700 | 73 | 36 | 72 |
| 7200 | 73 | 98 | 46 |
| 4600 | 73 | 63 | 1 |
Таким образом, получим цифры результата (из 3-й колонки) 01369863 или с учётом 108 это 1.3698630-02. Если вычислить 1/73 напрямую, то получим то же.
В нашем случае вместо единицы будем использовать 1091.
Делать будем целочисленно группами по 6 цифр, первая группа может содержать 7.
Получится 107 и 14 раз по 106.
В каждой группе результат (целый, 6 знаков) сохраняется в регистре, а остаток переносится
.
Таким образом пробегаем по всем группам (регистрам). Итог деления - в регистрах, и остаток в стеке.
Повторяем N (65) раз с уменьшением N.
Алгоритм целочисленного деления и нахождения остатка выполняется в подпрограмме, а накопленный остаток хранится в стеке. В исходной программе делается многократный повтор: извлечь из очередного регистра текущие разряды, вызвать подпрограмму, сохранить. После пробега по всем разрядам уменьшить число N. При обнулении – закончить.
Я немного поменял роли. Остаток по-прежнему в стеке, но регистр R0 стал использовать для хранения числа N. Поэтому вычисляемые знаки, хранимые ранее в R0, тоже сохраняются в стеке в регистре RT. Какой вместительный стек :). Это позволило даже чуть-чуть сократить манипуляции в стеке, т.к. сохранять N уже не нужно, и конец цикла сделался проще.
Кроме того, чтобы сократить длину программы, многократный вызов подпрограммы с разными регистрами сделал во внутреннем цикле. А для счётчика этого цикла использовал два последних разряда регистра Re, потому что для вычислений используется только 6 разрядов.
Микрооптимизация: для зануления 15 регистров вместо явного числа 15 (две команды), используется дробное число 1/65 (функция обратной величины - одна команда), у которого в конце как раз 15. А для косвенной адресации без разницы.
По окончании хранимое в стеке начало числа e (в оригинале R0), нужно сбросить в R0. Да и в регистре Re нужно почистить хвост. Тут я немного сжульничал. Вместо прокрутки стека (из RZ в R0) и прибавления единицы, я вычислил средствами ПМК число e (как раз те первые 7 цифр, что в RT, уже с +1). Желающие могут проверить, что в стеке в RZ, RT то же самое, что в R0 (без +1). И вместо обнуления двух последних разрядов Re дописал два настоящих разряда числа e.
Как и в исходной программе результат хранится в регистрах R0…Re. Начало (R0) продублировано на экране (RX). Всего 94 цифры.
Для повтора вычислений нажмите В/0 С/П.
| # | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 |
|---|---|---|---|---|---|---|---|---|---|---|
| 00 | | Cx | 6 | 5 | x→П0 | F1/x | x→П1 | Cx | Кx→П1 | П→x1 | Fx=0 |
| 10 | | 06 | 1 | + | x→Пe | Cx | ВП | 7 | ПП | 52 | <-> |
| 20 | | В↑ | КП→xe | ПП | 52 | Кx→Пe | КЗН | П→xe | + | x→Пe | КП→xe |
| 30 | | − | Fx=0 | 21 | П→xe | ВП | /-/ | 2 | К[x] | ПП | 52 |
| 40 | | ВП | 2 | FL0 | 11 | 2 | 5 | + | x→Пe | КЗН | Fex |
| 50 | | x→П0 | С/П | <-> | F↻ | + | В↑ | П→x0 | ÷ | К[x] | П→x0 |
| 60 | | × | − | Fx≠0 | 66 | ВП | 6 | FВx | П→x0 | ÷ | В/О |
На входе в стеке предполагается:
| Регистр стека | Значение |
|---|---|
| RX | Очередные 6 разрядов, обозначим как K. |
| RY | Игнорируем. Мусор. Обозначим как M. |
| RZ | Накопленный остаток, может быть 8 разрядный. Обозначим как S. |
| RT | Сохранённые разряды первой группы. Как R0 в оригинале. Обозначим как H. |
На выходе должны получить:
| Регистр стека | Значение |
|---|---|
| RX | Новое K = [(K+S)/N], результат целочисленного деления. |
| RY | Новый S, как остаток от деления выше, уже умноженный на 106. |
| RZ | H. |
| RT | H. |
Кстати, небольшие уточнения. Первое, на входе RX и RZ можно поменять местами, что и делается в самом начале (первый вызов подпрограммы). Второе, делитель состоит из 2 знаков, остаток может тоже. При умножении на 106 уже 8 знаков. Это предел для ПМК. Поэтому группы по 6 знаков, а не по 7. В первой группе явно начинаем с 107, поэтому её это ограничение не касается. В первой группе может быть, а в конце и будет, 7 знаков.
Теперь по порядку, что выполняем в подпрограмме:
N (=65) загоняем в R0. Теперь нужно очистить 15 регистров. Это делают команды по адресам 06..10, но в R1 будет не 15, а 1/65 = 1.5384615^-02, которое и содержит 15 (в конце). Для косвенной адресации это не важно (см. Число положительное, но меньше единицы). Цикл очистки прервется, когда занулится сам R1.
С адреса 11. Как упоминалось, Re используется как счетчик внутреннего цикла по всем регистрам. Тут предполагается, что RX = Re×100. В самом начале это ноль, что верно.
Начальный индекс 1 сохраняется в Re. Чтобы не потерять из RT число H, 107 получаем через ВП без сдвига стека. После Cx это безопасно. Напомню, в RZ хранится первая группа H (в самом начале ноль), RY забили мусором (новое Re), и начинаем с 107. Это как раз случай, где входные RX, RZ для подпрограммы идут в другом порядке.
После вычисления нового H командами по адресам 19..20 загоняем его в RZ.
Число S остается в RX и RY. Дубль S потом потеряется
.
Теперь внутренний цикл по всем регистрам начиная с адреса 21. Извлекаем очередные 6 знаков, вызываем подпрограмму, сохраняем (адреса 21..24). Теперь нужно увеличить Re на 1. Чтобы не портить стек, превращаем число (всегда положительное) в 1 через КЗН. Новое Re сохраняем (команды 25..28).
Чтобы определить, что внутренний цикл закончен, косвенно извлекаем по Re. Если это само Re и есть, то дошли до конца. Что и делает проверка по адресу 31.
Для Re нужно отрезать две последние цифры, что делается по адресам 34..37. Здесь и далее сознательно используем опасную в программном режиме ВП, вместо F10x, чтобы не сдвигать стек и не потерять значение H. После П→xe команда ВП безопасна.
После последнего вызова подпрограммы
умножаем на 100 новое Re. Здесь, после возврата из подпрограммы,
команда ВП опять безопасна.
И возвращаемся на начало внешнего цикла, если не кончился
N.
При выходе из цикла (адрес 44), с учётом того, что Re уже умножен на 100,
последние цифры числа e прибавляем. И делаем жульничество
,
описанное в разделе по оптимизации.
Всё.